package edu.kit.aifb.ldstream.stat.util;

import java.util.HashMap;
import java.util.Map;

import dk.ange.octave.OctaveEngine;
import dk.ange.octave.OctaveEngineFactory;
import dk.ange.octave.type.OctaveDouble;

import edu.kit.aifb.ldstream.stat.distrib.BinomialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ContinuousUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.DiscreteUniformDistribution;
import edu.kit.aifb.ldstream.stat.distrib.ExponentialDistribution;
import edu.kit.aifb.ldstream.stat.distrib.IDistribution;
import edu.kit.aifb.ldstream.stat.distrib.NormalDistribution;
import edu.kit.aifb.ldstream.stat.distrib.PoissonDistribution;

public class OctaveFrontend implements IDistributionChecker {
	
	private OctaveEngine octave;
	
	public OctaveFrontend(){
		this.octave = new OctaveEngineFactory().getScriptEngine();
		// disable the echo output
		this.octave.setWriter(null);
		// disable the warnings, specially the warning for ties in the data set.
		this.octave.setErrorWriter(null);
	}
	
	
	@Override
	public synchronized IDistribution continuous(double[] values) {

		//  putting the values vector to octave dimension [values.length, 1]
		OctaveDouble a = new OctaveDouble(values, values.length, 1);
		octave.put("a", a);
		// calculating the mean of the values. 
		octave.eval("mean = mean(a)");
		double mean = octave.get(OctaveDouble.class, "mean").get(1,1);
		//now getting std
		octave.eval("std = std(a)");
		double std = octave.get(OctaveDouble.class, "std").get(1,1);
		
		// checking for normal distribution
		octave.eval("[pvaln,ksn,dn] = kolmogorov_smirnov_test_2(a, normrnd(mean, std, size(a,1), 1))");
		double normD = octave.get(OctaveDouble.class, "dn").get(1,1);
		
		// checking for exponential distribution. 
		// lambda = 1/mean(x)  because of the maximum-likelihood-estimation
		double expD = 1d;
		if(mean > 0){
			octave.eval("[pvalexp,ksexp,dexp] = kolmogorov_smirnov_test_2(a, exprnd( 1 / mean, size(a,1), 1))");
			expD = octave.get(OctaveDouble.class, "dexp").get(1,1);
		}
		octave.eval("min = min(a)");
		double min = octave.get(OctaveDouble.class, "min").get(1,1);
		octave.eval("max = max(a)");
		double max = octave.get(OctaveDouble.class, "max").get(1,1);
		
		// checking for uniform distribution
		octave.eval("[pvalu,ksu,du] = kolmogorov_smirnov_test_2(a, unifrnd( min, max, size(a,1), 1))");
		double unifD = octave.get(OctaveDouble.class, "du").get(1,1);
		
		/*
		 * Calculations are done now find the best fitting Distribution.
		 */
		IDistribution distrib;
		
		if(normD < expD){
			if(normD < unifD){
				// normD is the smallest difference
				distrib = new NormalDistribution(mean, std);
			}else{
				//unifD is the smallest
				distrib = new ContinuousUniformDistribution(min, max);
			}
		}else{
			// expD < normD
			if(expD < unifD){
				// expD is the smallest difference 
				distrib = new ExponentialDistribution(1.0 / mean);
			}else{
				// unifD is the smallest difference
				distrib = new ContinuousUniformDistribution(min, max);	
			}
		}
		// reset the octave instance
		octave.eval("clear");
		return distrib;
	}



	@Override
	public synchronized <T> IDistribution discrete(T[] values) {
		// calculate the occurences of the the same values
		double[] occurrences = countOccurences(values);
		//get the frequency of occurrence
		double[] pValues = pValues(occurrences);
		
		OctaveDouble a = new OctaveDouble(occurrences, occurrences.length, 1);
		octave.put("a", a);
		
		OctaveDouble p = new OctaveDouble(pValues, pValues.length, 1);
		octave.put("p", p);
		
		octave.eval("mean = mean(a)");
		double meanA = octave.get(OctaveDouble.class, "mean").get(1,1);
		
		octave.eval("meanp = sum(p) / size(p,1)");
		double meanP = octave.get(OctaveDouble.class, "meanp").get(1,1);

		octave.eval("max = max(a)");
		double max = octave.get(OctaveDouble.class, "max").get(1,1);
		
		octave.eval("sum = sum(a)");
		double sum = octave.get(OctaveDouble.class, "sum").get(1,1);
		
		// poisson
		octave.eval("[pvalpois, kspois, dpois] = kolmogorov_smirnov_test_2(a, poissrnd(mean, size(a,1),1))");
		double dPois = octave.get(OctaveDouble.class, "dpois").get(1,1);
		
		//discrete uniform
		octave.eval("[pvalunif, ksunif, dunif] = kolmogorov_smirnov_test_2(a, unidrnd(max, size(a,1),1))");
		double dUnif = octave.get(OctaveDouble.class, "dunif").get(1,1);
		
		// binomial distribution
		octave.eval("[pvalbinom, ksbinom, dbinom] = kolmogorov_smirnov_test_2(a, binornd(sum, meanp, size(a,1), 1) )");
		double dBinom = octave.get(OctaveDouble.class, "dbinom").get(1,1);
		
		IDistribution distrib;
		if(dPois < dUnif){
			if(dPois < dBinom){
				// dPois < all
				distrib = new PoissonDistribution(meanA);
			}else{
				// dBinom < all
				distrib = new BinomialDistribution(meanP, (int) sum);
			}
		}else{
			if(dUnif < dBinom){
				// dUnif < all
				distrib = new DiscreteUniformDistribution(max);
			}else{
				// dBinom < all
				distrib = new BinomialDistribution(meanP, (int) sum);
			}
		}	
		// reset the octave instance
		octave.eval("clear");
		
		return distrib;
	}
	
	private  double[] pValues(double[] occurrences){
		double sum = 0;
		// first we need to know the amount of samples
		for(int i = 0; i < occurrences.length; i++){
			sum += occurrences[i];
		}
		double[] p = new double[occurrences.length];
		for(int i = 0; i < p.length; i++){
			p[i] = occurrences[i] / sum;
		}
		
		return p;
	}

	private  <T> double[] countOccurences(T[] values){
		Map<T, Integer> occurences = new HashMap<T, Integer>();
		int num;
		// count the occurrences of the given values
		for(T entry : values){
			if(occurences.containsKey(entry)){
				num = occurences.get(entry);
				occurences.put(entry, num + 1);
			} else{
				occurences.put(entry, 1);
			}
		}
		double[] samples = new double[occurences.size()];
		int i = 0;
		for(double entry : occurences.values()){
			samples[i++] = (double) entry;
		}

		return samples;

	}
	
	
	public void shutdown(){
		this.octave.close();
	}
	
	
}
